1"use client";
2
3
12import { useState } from "react";
13import { useParams, useRouter } from "next/navigation";
14import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
15import { History, Plus, Trash2 } from "lucide-react";
16import { toast } from "@/stores/ui-store";
17import { apiFetch } from "@/lib/api";
18import { useWorkspace } from "@/hooks/use-workspace";
19import { SettingsShell, Section } from "@/components/settings/settings-shell";
20import { statusCls, runStatusLabel, type AutomationSummary } from "@/components/automations/lib";
21import { RunHistory } from "@/components/automations/run-history";
22
23export default function AutomationsListPage() {
24 const params = useParams<{ workspace: string }>();
25 const slugOrId = params?.workspace ?? "";
26 const router = useRouter();
27 const { data: ws, isLoading } = useWorkspace(slugOrId);
28 const wid = ws?.id ?? "";
29
30 return (
31 <SettingsShell
32 title="Automations"
33 description="Automate record, page, form and schedule events."
34 active="automations"
35 >
36 {isLoading || !ws ? (
37 <p className="text-sm text-muted-foreground">Loading…</p>
38 ) : (
39 <AutomationsTable workspaceSlug={slugOrId} workspaceId={wid} onNew={() => router.push(`/w/${slugOrId}/settings/automations/new`)} />
40 )}
41 </SettingsShell>
42 );
43}
44
45function AutomationsTable({ workspaceSlug, workspaceId, onNew }: { workspaceSlug: string; workspaceId: string; onNew: () => void }) {
46 const qc = useQueryClient();
47 const [cursor, setCursor] = useState<string | null>(null);
48 const [expanded, setExpanded] = useState<string | null>(null);
49
50 const list = useQuery({
51 queryKey: ["workspaces", workspaceId, "automations", cursor ?? "first"],
52 queryFn: async (): Promise<{ data: AutomationSummary[]; nextCursor: string | null }> => {
53 const res = await apiFetch<{ data: AutomationSummary[]; nextCursor: string | null }>(
54 `/workspaces/${workspaceId}/automations?limit=50${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`
55 );
56 return res;
57 },
58 enabled: !!workspaceId
59 });
60
61 const rows = list.data?.data ?? [];
62 const nextCursor = list.data?.nextCursor ?? null;
63
64 const setStatus = useMutation({
65 mutationFn: (input: { id: string; status: string }) =>
66 apiFetch(`/automations/${input.id}/status`, { method: "POST", body: { status: input.status } }),
67 onSuccess: () => qc.invalidateQueries({ queryKey: ["workspaces", workspaceId, "automations"] }),
68 onError: (err) => toast("error", "Status change failed", err instanceof Error ? err.message : undefined)
69 });
70
71 const remove = useMutation({
72 mutationFn: (id: string) => apiFetch(`/automations/${id}`, { method: "DELETE" }),
73 onSuccess: () => {
74 qc.invalidateQueries({ queryKey: ["workspaces", workspaceId, "automations"] });
75 },
76 onError: (err) => toast("error", "Delete failed", err instanceof Error ? err.message : undefined)
77 });
78
79 return (
80 <Section title="Automations" description="Trigger → condition → actions. Runs are recorded with per-step logs.">
81 <div className="mb-3 flex items-center justify-between">
82 <span className="text-xs text-muted-foreground">{rows.length} automation{rows.length === 1 ? "" : "s"}</span>
83 <button
84 type="button"
85 data-testid="automation-new"
86 className="inline-flex items-center gap-1 rounded-md bg-primary px-2.5 py-1.5 text-sm font-medium text-white hover:opacity-90"
87 onClick={onNew}
88 >
89 <Plus className="size-4" /> New automation
90 </button>
91 </div>
92
93 {list.isLoading ? (
94 <p className="text-sm text-muted-foreground">Loading…</p>
95 ) : (
96 <div className="overflow-x-auto">
97 <table className="w-full border-collapse text-sm" data-testid="automation-list">
98 <thead>
99 <tr className="border-b border-border text-left text-xs text-muted-foreground">
100 <th className="px-2 py-1.5 font-medium">Name</th>
101 <th className="px-2 py-1.5 font-medium">Status</th>
102 <th className="px-2 py-1.5 font-medium">Triggers</th>
103 <th className="px-2 py-1.5 font-medium">Latest run</th>
104 <th className="px-2 py-1.5" />
105 </tr>
106 </thead>
107 <tbody>
108 {rows.length === 0 && (
109 <tr>
110 <td colSpan={5} className="px-2 py-6 text-center text-sm text-muted-foreground">
111 No automations yet. Create one to start automating.
112 </td>
113 </tr>
114 )}
115 {rows.map((a) => (
116 <AutomationRow
117 key={a.id}
118 a={a}
119 workspaceSlug={workspaceSlug}
120 expanded={expanded === a.id}
121 onToggleExpand={() => setExpanded(expanded === a.id ? null : a.id)}
122 onStatus={(status) => setStatus.mutate({ id: a.id, status })}
123 onRemove={() => {
124 if (window.confirm(`Delete “${a.name}”? This archives it (runs are kept).`)) remove.mutate(a.id);
125 }}
126 busy={setStatus.isPending}
127 />
128 ))}
129 </tbody>
130 </table>
131 </div>
132 )}
133
134 {nextCursor && (
135 <button
136 type="button"
137 className="mt-3 text-xs text-muted-foreground hover:text-foreground"
138 onClick={() => setCursor(nextCursor)}
139 >
140 Load more…
141 </button>
142 )}
143 </Section>
144 );
145}
146
147function AutomationRow({
148 a,
149 workspaceSlug,
150 expanded,
151 onToggleExpand,
152 onStatus,
153 onRemove,
154 busy
155}: {
156 a: AutomationSummary;
157 workspaceSlug: string;
158 expanded: boolean;
159 onToggleExpand: () => void;
160 onStatus: (status: string) => void;
161 onRemove: () => void;
162 busy: boolean;
163}) {
164 const router = useRouter();
165 const active = a.status === "ACTIVE";
166
167 return (
168 <>
169 <tr data-testid={`automation-row-${a.id}`} className="border-b border-border/60 hover:bg-muted/30">
170 <td className="max-w-52 px-2 py-2">
171 <button
172 type="button"
173 className="block truncate text-left font-medium hover:underline"
174 onClick={() => router.push(`/w/${workspaceSlug}/settings/automations/${a.id}`)}
175 title={a.name}
176 >
177 {a.name}
178 </button>
179 {a.description ? <span className="mt-0.5 block truncate text-xs text-muted-foreground">{a.description}</span> : null}
180 </td>
181 <td className="px-2 py-2">
182 <span className={`inline-block rounded-full px-2 py-0.5 text-[11px] font-medium ${statusCls(a.status)}`}>{a.status}</span>
183 </td>
184 <td className="px-2 py-2">
185 <span className="block max-w-40 truncate text-xs text-muted-foreground" title={a.triggers.join(", ")}>
186 {a.triggers.length > 0 ? a.triggers.join(", ") : "—"}
187 </span>
188 </td>
189 <td className="px-2 py-2">
190 {a.latestRun ? (
191 <span className="text-xs text-muted-foreground">{runStatusLabel(a.latestRun.status)}</span>
192 ) : (
193 <span className="text-xs text-muted-foreground/60">never</span>
194 )}
195 </td>
196 <td className="px-2 py-2">
197 <div className="flex items-center justify-end gap-2">
198 <button
199 type="button"
200 data-testid={`automation-toggle-${a.id}`}
201 role="switch"
202 aria-checked={active}
203 aria-label={`Enable / pause ${a.name}`}
204 onClick={() => onStatus(active ? "PAUSED" : "ACTIVE")}
205 disabled={busy}
206 className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full transition ${
207 active ? "bg-primary" : "bg-muted"
208 } ${busy ? "opacity-50" : ""}`}
209 >
210 <span
211 className={`inline-block h-4 w-4 transform rounded-full bg-white transition ${active ? "translate-x-4" : "translate-x-0.5"}`}
212 />
213 </button>
214 <button
215 type="button"
216 data-testid={`automation-runs-${a.id}`}
217 className="inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs text-muted-foreground hover:text-foreground"
218 onClick={onToggleExpand}
219 aria-expanded={expanded}
220 >
221 <History className="size-3.5" /> Runs
222 </button>
223 <button
224 type="button"
225 className="rounded-md p-1 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
226 onClick={onRemove}
227 aria-label={`Delete ${a.name}`}
228 >
229 <Trash2 className="size-4" />
230 </button>
231 </div>
232 </td>
233 </tr>
234 {expanded && (
235 <tr className="border-b border-border/60 bg-muted/20">
236 <td colSpan={5} className="px-2 py-2">
237 <RunHistory automationId={a.id} compact />
238 </td>
239 </tr>
240 )}
241 </>
242 );
243}
244